Skip to content

Conversation

@stoneTiger0912
Copy link
Member

@stoneTiger0912 stoneTiger0912 commented Aug 22, 2025

📝 변경 내용


✅ 체크리스트

  • 코드가 정상적으로 동작함
  • 테스트 코드 통과함
  • 문서(README 등)를 최신화함
  • 코드 스타일 가이드 준수

💬 기타 참고 사항

Summary by CodeRabbit

  • 신기능

    • 그룹 멤버 조회 API 추가: GET /v1/groups/{groupId}/members. 멤버 ID, 역할, 이름, 프로필 정보를 반환합니다.
    • 그룹 멤버 목록 응답 DTO 및 관련 서비스 흐름 추가로 멤버 목록 조회 지원.
  • Chores

    • QueryDSL 도입 및 빌드 설정 추가로 코드 생성 소스 경로 관리 및 정리 자동화.
    • 생성된 소스 디렉터리 무시(.gitignore) 항목 추가.
  • 테스트

    • 그룹 멤버 조회 성공 시나리오 테스트 추가로 안정성 강화.

@stoneTiger0912 stoneTiger0912 self-assigned this Aug 22, 2025
@stoneTiger0912 stoneTiger0912 added the enhancement New feature or request label Aug 22, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 22, 2025

Walkthrough

QueryDSL JPA를 도입해 생성 소스 경로를 빌드에 연결하고(.gitignore 포함), 그룹 멤버 조회 API(GET /v1/groups/{groupId}/members)를 추가했다. DTO(FindGroupMemberResponse, GroupMemberInfo), 레포지토리 커스텀 인터페이스/구현(QueryDSL), 서비스/컨트롤러 확장 및 관련 테스트가 추가되었다.

Changes

Cohort / File(s) Summary
Build & Config
build.gradle, .gitignore, src/main/java/project/flipnote/infra/config/QuerydslConfig.java
QueryDSL JPA 의존성 및 어노테이션 프로세서 추가, 생성 소스 디렉터리(build/generated/querydsl) 설정·클린 처리, /src/main/generated/.gitignore에 추가. JPAQueryFactory 빈 등록.
Controller
src/main/java/project/flipnote/group/controller/GroupController.java
GET /v1/groups/{groupId}/members 엔드포인트 추가(인증 원리 전달, FindGroupMemberResponse 반환).
Service
src/main/java/project/flipnote/group/service/GroupService.java
기존 validateGroupInUser를 반환 없는 검증 메서드로 추가하고(원래 기능은 getGroupMember로 분리), 그룹 멤버 목록 조회용 findGroupMembers(AuthPrinciple, Long) 추가 및 그룹 존재/멤버 검증 로직 보완. 일부 메서드명 변경(validateGroupgetGroup) 및 호출부 업데이트.
Repository API
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java, src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java, src/main/java/project/flipnote/group/repository/GroupRepository.java
GroupMemberRepositoryGroupMemberRepositoryCustom을 확장하도록 변경, 여러 파생 쿼리 메서드 추가/명명 정리(existsByGroup_IdAndUser_Id, findByGroup_IdAndUser_Id, countByGroup_IdAndUser_idNot 등). 커스텀 인터페이스에 findGroupMembers(Long) 선언 추가. GroupRepositoryexistsByIdAndDeletedAtIsNull(Long) 추가.
Repository Impl (QueryDSL)
src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java
QueryDSL(JPAQueryFactory)을 사용해 userProfile 및 groupMember를 조인하여 GroupMemberInfo로 프로젝션한 List<GroupMemberInfo> 반환하는 구현 추가.
Model / DTOs
src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java, src/main/java/project/flipnote/group/model/GroupMemberInfo.java
멤버 목록 응답 레코드(FindGroupMemberResponse) 및 멤버 정보 레코드(GroupMemberInfo) 추가(정적 팩토리 메서드 포함).
Tests
src/test/java/project/flipnote/group/service/GroupServiceTest.java
그룹 멤버 조회 성공 케이스 추가, GroupMemberRepositoryImpl 모킹/ID 설정 및 응답 검증 추가. 기존 테스트에서 리포지토리 호출/존재 검사 호출 형태를 ID 기반으로 수정.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client as 클라이언트
    participant Controller as GroupController
    participant Service as GroupService
    participant GroupRepo as GroupRepository
    participant UserRepo as UserProfileRepository
    participant MemberRepo as GroupMemberRepository
    participant MemberRepoImpl as GroupMemberRepositoryImpl
    participant QF as JPAQueryFactory

    Client->>Controller: GET /v1/groups/{groupId}/members
    Controller->>Service: findGroupMembers(auth, groupId)
    Service->>GroupRepo: existsByIdAndDeletedAtIsNull(groupId) / getGroup(groupId)
    GroupRepo-->>Service: Group
    Service->>UserRepo: findById(auth.userId)
    UserRepo-->>Service: UserProfile
    Service->>MemberRepo: validateGroupInUser(userProfile, groupId) / getGroupMember(...)
    MemberRepo-->>Service: GroupMember (검증)
    Service->>MemberRepoImpl: findGroupMembers(groupId)
    MemberRepoImpl->>QF: QueryDSL select+join (userProfile, groupMember)
    QF-->>MemberRepoImpl: List<GroupMemberInfo>
    MemberRepoImpl-->>Service: List<GroupMemberInfo>
    Service-->>Controller: FindGroupMemberResponse.from(list)
    Controller-->>Client: 200 OK (FindGroupMemberResponse)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • dungbik

Poem

깡총깡총 토끼가 말하네:
쿼리씨앗 심어 길을 냈고 🥕
멤버들 이름·프사 한 줄로 모아,
컨트롤러가 웃으며 돌려주네.
머지되면 당근 한 입 찹! 🐰

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/find-group-member

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (18)
.gitignore (1)

8-10: QueryDSL 생성 경로(.gitignore)와 빌드 설정(build.gradle) 간 불일치 정리 제안

현재 빌드가 QueryDSL 소스를 build/generated/querydsl 아래로 생성하도록 설정되어 있는데(.gradle/clean도 포함), .gitignore에는 /src/main/generated/만 무시하도록 추가되어 있습니다. 실사용 경로와 무관한 ignore 규칙은 오해를 유발할 수 있어 정리하는 편이 좋습니다. build/ 전체가 이미 무시되므로 별도 조치가 없어도 생성 파일은 커밋되지 않습니다.

아래 중 하나로 정리하는 것을 권장합니다.

  • /src/main/generated/ 규칙 제거
  • 혹은 주석을 추가해 “현재는 build 경로를 사용함”을 명시

적용 예시(diff):

 ### QueryDSL ###
-/src/main/generated/
+# Generated Q-types are emitted to build/generated/querydsl (see build.gradle).
+# Keep the following rule only if you switch generation to src/main/generated in the future.
+# /src/main/generated/
src/main/java/project/flipnote/infra/config/QuerydslConfig.java (1)

11-20: JPAQueryFactory 빈 등록은 적절합니다 + 생성자 주입 간소화 제안

구성 자체는 표준적이고 문제 없습니다. 사소 제안으로, 설정 클래스에서 Lombok과 필드 보관을 생략하고 “메서드 파라미터 주입”을 사용하면 의존성 표면을 더 줄일 수 있습니다. 기능은 동일합니다.

간소화 예시(diff):

 package project.flipnote.infra.config;

 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;

 import com.querydsl.jpa.impl.JPAQueryFactory;

-import jakarta.persistence.EntityManager;
-import lombok.RequiredArgsConstructor;
+import jakarta.persistence.EntityManager;

 @Configuration
-@RequiredArgsConstructor
 public class QuerydslConfig {
-	private final EntityManager entityManager;
-
 	@Bean
-	public JPAQueryFactory jpaQueryFactory() {
-		return new JPAQueryFactory(entityManager);
+	public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
+		return new JPAQueryFactory(entityManager);
 	}
 }
build.gradle (3)

55-59: QueryDSL JPA 의존성 구성 적절

querydsl-jpa:5.0.0:jakarta 및 APT 설정이 Spring Boot 3.x(Jakarta) 환경과 맞습니다. 현재 형태로 충분히 빌드·생성이 동작할 것으로 보입니다.
선택사항: 버전 상수를 ext(또는 version catalog)로 올려서 중앙 관리하면 추후 업그레이드가 수월합니다.


75-76: clean 훅 중복 정리 제안

build/ 전체를 이미 Gradle 기본 clean이 제거하므로 file(querydslDir).deleteDir()는 실질적으로 중복입니다. 제거해도 무방합니다.

정리 예시(diff):

-clean.doLast {
-    file(querydslDir).deleteDir()
-}

63-65: 생성 디렉터리 설정 범위 축소(compileJava 한정) 권장 및 검증 요청

파일: build.gradle (63–65줄)

문제 요약

  • tasks.withType(JavaCompile).configureEach { … }를 사용하면 compileJava뿐 아니라 compileTestJava까지 동일한 build/generated/querydsl 디렉터리를 생성 경로로 강제 적용합니다.
  • 테스트 소스에서 APT가 동작할 경우(main/test 산출물이 한 폴더에 섞이는) 충돌 가능성이 있습니다.

권장 수정(diff)

 def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile

- tasks.withType(JavaCompile).configureEach {
-     options.generatedSourceOutputDirectory.set(querydslDir)
- }
+ tasks.named('compileJava') {
+     options.generatedSourceOutputDirectory.set(querydslDir)
+ }
+ // 테스트용 Q-타입은 별도 디렉터리로 분리 가능:
+ // tasks.named('compileTestJava') {
+ //     options.generatedSourceOutputDirectory.set(
+ //         layout.buildDirectory.dir("generated/querydslTest").get().asFile
+ //     )
+ // }

검증 요청
프로젝트를 실제로 빌드한 후(./gradlew compileJava compileTestJava),
build/generated/querydsl 디렉터리 내에 테스트 전용 Q-타입 파일이 섞여 있는지 직접 확인해 주세요.

src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java (1)

7-9: 반환 타입 명확성을 드러내는 네이밍/주석 제안

findGroupMembers(Long groupId)는 엔티티가 아닌 DTO(GroupMemberInfo)를 반환합니다. 혼동 방지를 위해 JavaDoc 추가 또는 메서드명을 findGroupMemberInfos(Long groupId)처럼 더 구체화하는 것을 고려해볼 수 있습니다. 기능상 문제는 없습니다.

예시:

/**
 * 주어진 그룹의 멤버 정보를 DTO로 조회한다.
 * @param groupId 그룹 ID
 * @return GroupMemberInfo 목록
 */
List<GroupMemberInfo> findGroupMembers(Long groupId);
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)

28-29: 엔티티 조회 메서드 사용 여부 검증 및 네이밍 일관성 제안

검증 결과, 새로 추가된 GroupMemberRepository.findAllByGroup_Id(Long)는 코드 내에서 호출되지 않습니다. 따라서 당장 사용처가 없다면 API 증가를 막기 위해 해당 메서드 추가를 보류하거나 삭제를 고려해 주세요.

또한, 기존 메서드들의 Id 대소문자 표기 규칙이 혼재되어 있어 가독성이 떨어집니다. 아래 항목을 참고해 이후 리팩터링을 권장드립니다.

  • 호출부 확인 결과
    • findAllByGroup_Id 호출부 없음
      (rg -n '\bfindAllByGroup_Id\s*\(' -C2 src/main/java 결과)
  • 네이밍 일관성
    • existsByGroup_idAndUser_id, countByGroup_idAndUser_idNot
    • 신규 메서드: findAllByGroup_Id
      → 모두 Id 대문자 표기(예: Group_Id, User_Id) 또는 소문자 표기(예: group_id, user_id) 중 하나로 통일
src/test/java/project/flipnote/group/service/GroupServiceTest.java (2)

73-75: 불필요한 Mock 제거 제안 (GroupMemberRepositoryImpl는 사용되지 않음)

@Mock GroupMemberRepositoryImpl는 현재 테스트 어디에서도 사용되지 않습니다. 혼동을 줄이기 위해 제거하는 것이 좋습니다.

다음과 같이 정리 가능합니다:

-	@Mock
-	GroupMemberRepositoryImpl groupMemberRepositoryImpl;

317-351: 그룹 멤버 조회 테스트 전반은 적절합니다. 다만 ID 의미와 테스트 안정성 확인 부탁드립니다.

  • 시나리오/검증은 명확합니다. 저장소 상호작용 검증도 OK입니다.
  • GroupMemberInfo.from(userProfile.getId(), ...)로 내려가는 id가 "사용자 ID"임을 전제하고 있습니다. API에서 id가 "그룹멤버 엔티티 ID"가 아닌 "사용자 ID"임이 의도인지 확인 부탁드립니다. 이후 변경이 어렵기 때문에 합의가 필요합니다.
  • 테스트 안정성 관점에서 UserFixture.createActiveUser()가 ID를 세팅하지 않는 경우, authPrinciple.userId()가 null이 되어 저장소 스텁과 불일치할 수 있습니다. 픽스처가 ID를 보장하지 않는다면, 아래와 같이 명시 세팅을 권장합니다.
// before() 내 보완 예시
ReflectionTestUtils.setField(userProfile, "id", 1L);
authPrinciple = new AuthPrinciple(1L, 1L, userProfile.getEmail(), AccountRole.USER, 1L);
src/main/java/project/flipnote/group/controller/GroupController.java (1)

17-19: 중복 import 제거

FindGroupMemberResponse가 두 번 import 되었습니다. 한 줄을 제거해 주세요.

 import project.flipnote.group.model.FindGroupMemberResponse;
-import project.flipnote.group.model.FindGroupMemberResponse;
src/main/java/project/flipnote/group/model/GroupMemberInfo.java (1)

5-14: 필드 명확성 개선: id → userId, profile → profileImageUrl (권장)

현재 id가 사용자 ID를 의미합니다. 외부 API 스키마에서 혼동을 줄이려면 보다 구체적인 필드명이 좋습니다. 새 기능이므로 지금 변경하는 편이 장기 유지보수에 유리합니다.

-public record GroupMemberInfo(
-	Long id,
-	GroupMemberRole role,
-	String name,
-	String profile
-) {
-	public static GroupMemberInfo from(Long id, GroupMemberRole role, String name, String profile) {
-		return new GroupMemberInfo(id, role, name, profile);
+public record GroupMemberInfo(
+	Long userId,
+	GroupMemberRole role,
+	String name,
+	String profileImageUrl
+) {
+	public static GroupMemberInfo from(Long userId, GroupMemberRole role, String name, String profileImageUrl) {
+		return new GroupMemberInfo(userId, role, name, profileImageUrl);
 	}
 }

추가로 아래 사용처도 함께 업데이트가 필요합니다(예시):

  • GroupMemberRepositoryImpl: 생성자 프로젝션 인자 순서/이름
  • GroupServiceTest: res.groupMembers().get(0).userId()로 검증 변경
src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java (1)

5-11: 응답 리스트 방어적 복사 제안

외부에서 전달된 List가 가변인 경우를 방지하려면 정적 팩토리에서 방어적 복사를 권장합니다.

 public record FindGroupMemberResponse(
 	List<GroupMemberInfo> groupMembers
 ) {
 	public static FindGroupMemberResponse from(List<GroupMemberInfo> groupMembers) {
-		return new FindGroupMemberResponse(groupMembers);
+		return new FindGroupMemberResponse(List.copyOf(groupMembers));
 	}
 }
src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java (2)

22-34: 시그니처 구현 표식과 정렬 추가 제안

  • 인터페이스 구현 이후 @Override를 추가하면 오타/시그니처 변경 시 컴파일 타임에 잡힙니다.
  • 대규모 결과에 대한 일관 응답을 위해 정렬 기준(예: 역할, 이름)을 부여하는 것을 고려해 보세요.
-	public List<GroupMemberInfo> findGroupMembers(Long groupId) {
+	@Override
+	public List<GroupMemberInfo> findGroupMembers(Long groupId) {
 		return queryFactory.select(Projections.constructor(
 				GroupMemberInfo.class,
 				userProfile.id,
 				groupMember.role,
 				userProfile.name,
 				userProfile.profileImageUrl
 			))
 			.from(groupMember)
 			.join(groupMember.user, userProfile)
 			.where(groupMember.group.id.eq(groupId))
+			// 필요 시 정렬 예시:
+			// .orderBy(groupMember.role.asc(), userProfile.name.asc())
 			.fetch();
 	}

19-21: Q타입 상수화로 의도 명확화 (선택사항)

Q타입은 상태가 없으므로 static final로 선언하면 의도가 선명해집니다.

-QUserProfile userProfile = QUserProfile.userProfile;
-QGroupMember groupMember = QGroupMember.groupMember;
+private static final QUserProfile userProfile = QUserProfile.userProfile;
+private static final QGroupMember groupMember = QGroupMember.groupMember;
src/main/java/project/flipnote/group/service/GroupService.java (4)

56-63: 중복 로직 제거: 존재 검증은 getGroupMember 재사용 권장

validateGroupInUsergetGroupMember가 동일한 쿼리와 동일한 예외 흐름을 갖습니다. DRY 관점에서 validateGroupInUsergetGroupMember를 호출하도록 단순화하면 유지보수성이 좋아집니다. 또한 메서드 명은 일반적으로 validateUserInGroup가 더 자연스럽습니다. (리네임은 선택사항)

아래와 같이 단순화할 수 있습니다:

 public void validateGroupInUser(UserProfile user, Long groupId) {
-    groupMemberRepository.findByGroup_IdAndUser_Id(groupId, user.getId()).orElseThrow(
-        () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP)
-    );
+    // 존재 검증 로직을 단일 지점으로 집약
+    getGroupMember(user, groupId);
 }

245-259: 멤버 리스트 접근 권한 재검토 및 테스트 보강 제안

현재는 "그룹에 속해 있으면 모두 조회 가능" 정책입니다. 정책적으로 멤버 목록 열람 권한이 별도로 존재한다면(예: MEMBER_READ/USER_LIST_VIEW 등), hasPermission(groupId, user.getId(), <권한상수>) 체크를 추가해 주세요. 또한 노출 필드가 PII에 해당하지 않는지(DTO에 이메일/전화번호 등 포함 여부) 점검 바랍니다.

테스트 측면에서는 다음 케이스를 권장합니다:

  • 비멤버가 요청 시 USER_NOT_IN_GROUP 에러 반환
  • 삭제되었거나 존재하지 않는 그룹 요청 시 GROUP_NOT_FOUND 반환
  • 응답 정렬 기준이 있다면 정렬 일관성 검증

원하시면 권한 체크 및 위 음수 케이스(403/404 등) 단위/통합 테스트 템플릿을 추가로 드리겠습니다.


68-72: GroupService 내부 에러 코드 통일 및 테스트 수정 안내

아래 파일에서 GroupJoinErrorCode.USER_NOT_IN_GROUP 대신 GroupErrorCode.USER_NOT_IN_GROUP 을 사용하도록 리팩터링하고, 이에 맞춰 테스트도 함께 업데이트해주세요.

• src/main/java/project/flipnote/group/service/GroupService.java

  • validateGroupInUser (라인 61)
  • getGroupMember (라인 70)
 return groupMemberRepository.findByGroup_IdAndUser_Id(groupId, user.getId()).orElseThrow(
-    () -> new BizException(GroupJoinErrorCode.USER_NOT_IN_GROUP)
+    () -> new BizException(GroupErrorCode.USER_NOT_IN_GROUP)
 );

• src/test/java/project/flipnote/group/service/GroupServiceTest.java (라인 196~198)

  • import 구문 변경
  • assertion 수정
-import project.flipnote.groupjoin.error.GroupJoinErrorCode;
+import project.flipnote.group.error.GroupErrorCode;

...

-assertEquals(GroupJoinErrorCode.USER_NOT_IN_GROUP, exception.getErrorCode());
+assertEquals(GroupErrorCode.USER_NOT_IN_GROUP, exception.getErrorCode());

위 변경으로 GroupService 내부에서 사용하는 에러 코드를 GroupErrorCode 로 일관되게 유지하고, 테스트 오류를 방지할 수 있습니다.


188-195: 리팩토링 제안: private 메서드명 변경 및 정렬 보장 검토

명확성을 위해 Service 내 오버로드된 findGroupMembers(AuthPrinciple, Long) 메서드와 충돌하는 private 메서드를 loadGroupMemberInfos(또는 findGroupMemberInfos)로 변경하시길 권장합니다. 예:

-    /*
-     그룹 내 모든 멤버리스트 조회
-     */
-    private List<GroupMemberInfo> findGroupMembers(Long groupId) {
-        //각 그룹멤버의 id를 가지고 유저를 찾고 유저명, 권한, 등등 가져오기
-        return groupMemberRepository.findGroupMembers(groupId);
-    }
+    /*
+     그룹 내 모든 멤버 정보 로드
+     */
+    private List<GroupMemberInfo> loadGroupMemberInfos(Long groupId) {
+        return groupMemberRepository.findGroupMembers(groupId);
+    }
-    List<GroupMemberInfo> groupMembers = findGroupMembers(groupId);
+    List<GroupMemberInfo> groupMembers = loadGroupMemberInfos(groupId);

또한, GroupMemberRepositoryImpl#findGroupMembers는 QueryDSL의 DTO 프로젝션 및 join(groupMember.user, userProfile)으로 N+1 이슈를 방지하고 있으나, 현재 반환 순서가 미정의 상태입니다. 일관된 순서 보장이 필요하다면 아래와 같이 .orderBy(...)를 추가하세요:

return queryFactory
    .select(Projections.constructor(
        GroupMemberInfo.class,
        userProfile.id,
        groupMember.role,
        userProfile.name,
        userProfile.profileImageUrl
    ))
    .from(groupMember)
    .join(groupMember.user, userProfile)
    .where(groupMember.group.id.eq(groupId))
    .orderBy(
        // 예: 역할 순서(desc) → 가입일(오름차순)
        groupMember.role.desc(),
        userProfile.createdAt.asc()
    )
    .fetch();
  • 그룹 최대 인원(100명) 기준으로 페이지네이션은 필수는 아니나, 향후 확장 시 고려하시기 바랍니다.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 25bf154 and a1e12b7.

📒 Files selected for processing (11)
  • .gitignore (1 hunks)
  • build.gradle (1 hunks)
  • src/main/java/project/flipnote/group/controller/GroupController.java (2 hunks)
  • src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java (1 hunks)
  • src/main/java/project/flipnote/group/model/GroupMemberInfo.java (1 hunks)
  • src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2 hunks)
  • src/main/java/project/flipnote/group/repository/GroupMemberRepositoryCustom.java (1 hunks)
  • src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupService.java (5 hunks)
  • src/main/java/project/flipnote/infra/config/QuerydslConfig.java (1 hunks)
  • src/test/java/project/flipnote/group/service/GroupServiceTest.java (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/project/flipnote/infra/config/QuerydslConfig.java (1)
src/main/java/project/flipnote/group/repository/GroupMemberRepositoryImpl.java (1)
  • RequiredArgsConstructor (14-36)
src/main/java/project/flipnote/group/controller/GroupController.java (3)
src/main/java/project/flipnote/group/controller/GroupInvitationQueryController.java (1)
  • RequiredArgsConstructor (19-50)
src/main/java/project/flipnote/groupjoin/controller/GroupJoinController.java (1)
  • RestController (14-76)
src/main/java/project/flipnote/group/controller/GroupInvitationController.java (1)
  • RequiredArgsConstructor (23-63)
src/main/java/project/flipnote/group/model/FindGroupMemberResponse.java (2)
src/main/java/project/flipnote/groupjoin/model/FindGroupJoinListMeResponse.java (2)
  • FindGroupJoinListMeResponse (7-13)
  • from (10-12)
src/main/java/project/flipnote/groupjoin/model/GroupJoinListResponse.java (1)
  • GroupJoinListResponse (7-13)
src/main/java/project/flipnote/group/model/GroupMemberInfo.java (2)
src/main/java/project/flipnote/group/entity/GroupMember.java (2)
  • Getter (23-53)
  • Builder (47-52)
src/main/java/project/flipnote/group/entity/GroupMemberRole.java (1)
  • GroupMemberRole (3-5)
src/main/java/project/flipnote/group/service/GroupService.java (2)
src/main/java/project/flipnote/groupjoin/service/GroupJoinService.java (1)
  • Slf4j (41-286)
src/main/java/project/flipnote/group/service/GroupMemberPolicyService.java (1)
  • RequiredArgsConstructor (22-69)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
build.gradle (1)

67-73: sourceSets 연결 LGTM

생성 디렉터리를 main 소스셋에 연결한 방식이 표준적이고 IDE 인식에도 유리합니다.

src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)

15-15: 커스텀 레포지토리 구현 클래스 정상 인식 확인 완료

GroupMemberRepositoryImpl 클래스가 src/main/java/project/flipnote/group/repository 패키지에 올바르게 위치해 있으며, findGroupMembers 커스텀 메서드가 서비스와 컨트롤러 레이어에서 정상적으로 호출되는 것을 확인했습니다. Spring Data가 커스텀 구현을 인식하는 데 문제가 없으므로 변경 사항을 승인합니다.

src/test/java/project/flipnote/group/service/GroupServiceTest.java (1)

297-297: Reflection으로 그룹 ID 주입 OK

도메인 생성자를 해치지 않으면서 ID를 부여하는 합리적인 선택입니다.

src/main/java/project/flipnote/group/controller/GroupController.java (1)

60-68: GET /v1/groups/{groupId}/members 엔드포인트 추가 LGTM

서비스 위임과 응답 포맷이 명확하고, 기존 컨트롤러 스타일과 일관됩니다.

src/main/java/project/flipnote/group/service/GroupService.java (2)

20-24: DTO import 추가는 적절합니다.

새 API(response/vo) 사용을 위한 필요한 의존성만 추가되어 있어 깔끔합니다.


224-224: 메서드 명 변경 반영 OK

deleteGroup에서 기존 조회/검증 호출을 getGroupMember로 교체한 부분은 의도가 명확해졌고, 반환값을 즉시 활용 가능해져 좋습니다.

@stoneTiger0912 stoneTiger0912 requested a review from dungbik August 23, 2025 13:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)

25-27: deletedAt 기반 조회가 빈번하면 인덱스 고려

existsByIdAndDeletedAtIsNull가 트래픽 상위 호출이라면 (id, deletedAt) 복합 인덱스를 고려해 주세요. RDBMS에 따라 커버링/부분 인덱스로 존재 체크 비용을 더 낮출 수 있습니다.

src/test/java/project/flipnote/group/service/GroupServiceTest.java (3)

77-79: 불필요한 구현체(Mock) 제거

@Mock GroupMemberRepositoryImpl를 선언했지만 사용되지 않습니다. 서비스는 GroupMemberRepository(인터페이스)에 의존하므로 구현체 모킹은 중복·혼란을 유발합니다. 아래처럼 제거해 주세요.

- import project.flipnote.group.repository.GroupMemberRepositoryImpl;
...
-   @Mock
-   GroupMemberRepositoryImpl groupMemberRepositoryImpl;

Also applies to: 43-44


326-360: 그룹 멤버 조회 테스트 케이스 전반적으로 적절(LGTM) + 검증 강화 제안

기능 흐름(그룹 존재 → 유저 활성 → 그룹 멤버십 검증 → 멤버 목록 조회)을 충실히 모킹·검증했습니다. 권한 체크 호출에 대한 검증을 추가하면 테스트 신뢰도가 더 좋아집니다.

 then(groupRepository).should().existsByIdAndDeletedAtIsNull(group.getId());
 then(groupMemberRepository).should().findGroupMembers(group.getId());
+then(groupMemberRepository).should().existsByGroup_IdAndUser_Id(group.getId(), userProfile.getId());

385-385: 모킹 시 any() 대신 구체 값 사용

existsByIdAndDeletedAtIsNull(any())는 과도하게 관대한 매칭입니다. 명시적 groupId를 사용하면 회귀를 더 잘 포착합니다.

- given(groupRepository.existsByIdAndDeletedAtIsNull(any())).willReturn(true);
+ given(groupRepository.existsByIdAndDeletedAtIsNull(group.getId())).willReturn(true);
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)

20-29: 동일 의미의 파생 쿼리 중복 정의 제거 및 파라미터 명료화

existsByGroup_idAndUser_idexistsByGroup_IdAndUser_Id는 동일 의미의 파생 쿼리로 유지 시 혼동을 줍니다. 하나만 남기는 편이 좋습니다. 또한 existsByGroup_IdAndUser_Id의 두 번째 파라미터명이 id라 모호합니다. userId로 명확화해 주세요.

- boolean existsByGroup_idAndUser_id(Long groupId, Long userId);
...
- boolean existsByGroup_IdAndUser_Id(Long groupId, Long id);
+ boolean existsByGroup_IdAndUser_Id(Long groupId, Long userId);

추가로 네이밍 일관성 측면에서 countByGroup_idAndUser_idNotcountByGroup_IdAndUser_IdNot 형태로의 정렬을 권장합니다(서비스/테스트 호출부 동반 수정 필요).

src/main/java/project/flipnote/group/service/GroupService.java (3)

235-242: 동일 시그니처명 혼동 가능성: private 메서드 이름 개선 권장

public findGroupMembers(...)와 private findGroupMembers(...)가 공존해 가독성이 떨어질 수 있습니다. private 메서드를 fetchGroupMembers(또는 loadGroupMembers) 등으로 바꾸는 것을 권장합니다.

-private List<GroupMemberInfo> findGroupMembers(Long groupId) {
+private List<GroupMemberInfo> fetchGroupMembers(Long groupId) {
   //각 그룹멤버의 id를 가지고 유저를 찾고 유저명, 권한, 등등 가져오기
   return groupMemberRepository.findGroupMembers(groupId);
 }

303-307: 위 이름 변경에 따른 호출부 정렬

상기 메서드명을 변경했다면 호출부도 함께 수정되어야 합니다.

- List<GroupMemberInfo> groupMembers = findGroupMembers(groupId);
+ List<GroupMemberInfo> groupMembers = fetchGroupMembers(groupId);

292-307: 멤버 리스트 응답: 페이징/정렬 확장 포인트 고려

멤버 수가 많은 그룹을 고려하면 서버/네트워크 부하를 줄이기 위해 페이지네이션(Pageable)과 정렬(역할/이름 등) 파라미터를 노출할 여지가 있습니다. API/리포지토리 시그니처를 확장 가능하게 설계해 두면 향후 변경 비용을 줄일 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3a25542 and 34876aa.

📒 Files selected for processing (4)
  • src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (2 hunks)
  • src/main/java/project/flipnote/group/repository/GroupRepository.java (1 hunks)
  • src/main/java/project/flipnote/group/service/GroupService.java (6 hunks)
  • src/test/java/project/flipnote/group/service/GroupServiceTest.java (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/project/flipnote/group/repository/GroupRepository.java (2)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (2)
  • existsByGroup_idAndUser_id (18-18)
  • Repository (12-19)
src/main/java/project/flipnote/group/repository/GroupInvitationRepository.java (3)
  • existsByGroup_IdAndInviteeUserIdAndStatus (31-31)
  • GroupInvitationRepository (17-43)
  • existsByGroup_IdAndInviteeEmailAndStatus (33-33)
src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)
src/main/java/project/flipnote/groupjoin/repository/GroupJoinRepository.java (1)
  • Repository (12-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/main/java/project/flipnote/group/repository/GroupRepository.java (1)

25-27: 존재 여부 파생 쿼리 추가 적절(LGTM)

소프트 삭제(deletedAt) 조건을 포함한 존재 검증용 파생 쿼리 existsByIdAndDeletedAtIsNull(Long groupId) 추가가 서비스 계층의 검증 로직 단순화와 성능(불필요한 엔티티 로딩 제거)에 도움이 됩니다.

src/main/java/project/flipnote/group/repository/GroupMemberRepository.java (1)

15-15: 커스텀 리포지토리 확장(LGTM)

GroupMemberRepositoryCustom 확장으로 QueryDSL 기반 조회를 노출한 방향 좋습니다. 인터페이스 경계를 통해 서비스에서는 구현체 세부사항을 몰라도 됩니다.

src/main/java/project/flipnote/group/service/GroupService.java (3)

60-67: 멤버십 검증 메서드 분리 적절(LGTM)

validateGroupInUser로 멤버십 검증을 명확히 분리하여 재사용성과 가독성이 개선됐습니다. 에러코드도 GroupJoinErrorCode.USER_NOT_IN_GROUP으로 일관적입니다.


81-85: 그룹 존재 검증 메서드 추가 적절(LGTM)

존재만 확인할 때 existsBy...를 사용해 불필요한 엔티티 로딩을 피한 점 좋습니다. 호출부에서 후속 로딩이 필요 없는 경우에 적합합니다.


90-94: 그룹 엔티티 조회 메서드 분리 적절(LGTM)

상세가 필요한 경우 getGroup을 통해 not-found를 일관되게 처리합니다. 서비스 메서드별로 validateGroup/getGroup의 역할 구분이 명확합니다.

@stoneTiger0912 stoneTiger0912 merged commit 6dc7661 into develop Aug 25, 2025
3 checks passed
@stoneTiger0912 stoneTiger0912 deleted the feat/find-group-member branch August 25, 2025 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants